#include <ximera/system/application.hpp>
#include <ximera/system/isystemplugin.hpp>
#include <ximera/assert.hpp>
#include <ximera/common/utils.hpp>

namespace ximera { namespace system
{
    Application::Application()
        : _shouldClose(false), 
          _fps(0)
    {
        init();
    }

    Application::Application(AppSettings const& settings)
        : _settings(settings), _shouldClose(false), _fps(0)
    {
        init();
    }

    AppSettings& Application::settings()
    {
        return _settings;
    }

    AppSettings const& Application::settings() const
    {
        return _settings;
    }

    graphics::IGraphicsSystem* Application::graphicsSystem()
    {
        return _graphicsSystem.get();
    }

    input::IInputSystem* Application::inputSystem()
    {
        return _inputSystem.get();
    }

    audio::IAudioSystem* Application::audioSystem()
    {
        return _audioSystem.get();
    }

    scripting::IScriptingSystem* Application::scriptingSystem()
    {
        return _scriptingSystem.get();
    }

    gui::IGuiSystem* Application::guiSystem()
    {
        return _guiSystem.get();
    }

    physics::IPhysicsSystem* Application::physicsSystem()
    {
        return _physicsSystem.get();
    }

    void Application::processMessages()
    {
        _messageQueue.processMessages();
    }

    void Application::update()
    {
        Real secs = static_cast<Real>(_timer.elapsed());
        //if (secs < 0.0001f)
        //    secs = 0.006f;

        _timer.restart();
        _fps = 1.0f / secs;

        update(secs);
    }

    void Application::update(Real secs)
    {
        if (_inputSystem)
            _inputSystem->update();

        UpdateEventArgs e(*this, secs);
        _onUpdate(e);
    }

    void Application::render()
    {
        AppEventArgs e(*this);
        _onBeforeRender(e);

        if (_graphicsSystem)
            _graphicsSystem->render();

        if (_guiSystem)
            _guiSystem->render();

        _onAfterRender(e);
    }

    int Application::run()
    {
        try
        {
            _timer.restart();
            _shouldClose = false;
            _fps = 0;

            AppEventArgs e(*this);
            _onStart(e);

            while (!_shouldClose)
            {
                processMessages();

                update();

                render();
            }
            
            _onClose(e);
        }
        catch (ximera::Exception& e)
        {
            XIMERA_LOG() << e << std::endl;
        }
        catch (std::exception& e)
        {
            XIMERA_LOG() << e.what() << std::endl;
        }
        catch (...)
        {
            XIMERA_LOG() << "Unknown exception occured!" << std::endl;
        }

        return 0;
    }

    void Application::sendMessage(threading::IMessage* msg)
    {
        _messageQueue.addMessage(msg);
    }

    bool Application::shouldClose() const
    {
        return _shouldClose;
    }

    void Application::shouldClose(bool close)
    {
        _shouldClose = close;

        if (_shouldClose && !_onCloseRequest.empty())
        {
            CloseRequestEventArgs e(*this, _shouldClose);
            _onCloseRequest(e);
        }
    }

    Real Application::fps() const
    {
        return 0;
    }

    SignalAccess<Application::OnStartEvent> Application::onStart()
    {
        return _onStart;
    }

    SignalAccess<Application::OnUpdateEvent> Application::onUpdate()
    {
        return _onUpdate;
    }

    SignalAccess<Application::OnRenderEvent> Application::onBeforeRender()
    {
        return _onBeforeRender;
    }

    SignalAccess<Application::OnRenderEvent> Application::onAfterRender()
    {
        return _onAfterRender;
    }

    SignalAccess<Application::OnCloseRequestEvent> Application::onCloseRequest()
    {
        return _onCloseRequest;
    }

    SignalAccess<Application::OnCloseEvent> Application::onClose()
    {
        return _onClose;
    }

    void Application::addHandler(AppHandler& handler)
    {
        _onStart += boost::bind(&AppHandler::onStart, &handler, _1);
        _onUpdate += boost::bind(&AppHandler::onUpdate, &handler, _1);
        _onBeforeRender += boost::bind(&AppHandler::onBeforeRender, &handler, _1);
        _onAfterRender += boost::bind(&AppHandler::onAfterRender, &handler, _1);
        _onCloseRequest += boost::bind(&AppHandler::onCloseRequest, &handler, _1);
        _onClose += boost::bind(&AppHandler::onClose, &handler, _1);
    }

    void Application::removeHandler(AppHandler& handler)
    {
        _onStart -= boost::bind(&AppHandler::onStart, &handler, _1);
        _onUpdate -= boost::bind(&AppHandler::onUpdate, &handler, _1);
        _onBeforeRender -= boost::bind(&AppHandler::onBeforeRender, &handler, _1);
        _onAfterRender -= boost::bind(&AppHandler::onAfterRender, &handler, _1);
        _onCloseRequest -= boost::bind(&AppHandler::onCloseRequest, &handler, _1);
        _onClose -= boost::bind(&AppHandler::onClose, &handler, _1);
    }

    void Application::init()
    {
        try
        {
            if (_settings.contains(L"ximera.plugins.graphics"))
            {
                SharedPtr<ISystemPlugin> graphicsPlugin = 
                    dynamic_pointer_cast<ISystemPlugin>(_plugins.loadPlugin(_settings[L"ximera.plugins.graphics"]));

                if (graphicsPlugin)
                {
                    _graphicsSystem = SharedPtr<graphics::IGraphicsSystem>(
                        dynamic_cast<graphics::IGraphicsSystem*>(graphicsPlugin->createSystem(*this)));

                    XIMERA_CHECK(_graphicsSystem, XIMERA_THROW(InvalidParamException, L"Graphics plugin could not create Graphics system"));
                }
            }

            if (_settings.contains(L"ximera.plugins.input"))
            {
                SharedPtr<ISystemPlugin> inputPlugin = 
                    dynamic_pointer_cast<ISystemPlugin>(_plugins.loadPlugin(_settings[L"ximera.plugins.input"]));

                if (inputPlugin)
                {
                    _inputSystem = SharedPtr<input::IInputSystem>(
                        dynamic_cast<input::IInputSystem*>(inputPlugin->createSystem(*this)));

                    XIMERA_CHECK(_inputSystem, XIMERA_THROW(InvalidParamException, L"Input plugin could not create Input system"));
                }
            }

            if (_settings.contains(L"ximera.plugins.audio"))
            {
                SharedPtr<ISystemPlugin> audioPlugin = 
                    dynamic_pointer_cast<ISystemPlugin>(_plugins.loadPlugin(_settings[L"ximera.plugins.audio"]));

                if (audioPlugin)
                {
                    _audioSystem = SharedPtr<audio::IAudioSystem>(
                        dynamic_cast<audio::IAudioSystem*>(audioPlugin->createSystem(*this)));

                    XIMERA_CHECK(_audioSystem, XIMERA_THROW(InvalidParamException, L"Audio plugin could not create Audio system"));
                }
            }

            if (_settings.contains(L"ximera.plugins.scripting"))
            {
                SharedPtr<ISystemPlugin> scriptingPlugin = 
                    dynamic_pointer_cast<ISystemPlugin>(_plugins.loadPlugin(_settings[L"ximera.plugins.scripting"]));

                if (scriptingPlugin)
                {
                    _scriptingSystem = SharedPtr<scripting::IScriptingSystem>(
                        dynamic_cast<scripting::IScriptingSystem*>(scriptingPlugin->createSystem(*this)));

                    XIMERA_CHECK(_scriptingSystem, XIMERA_THROW(InvalidParamException, L"Scripting plugin could not create Scripting system"));
                }
            }

            if (_settings.contains(L"ximera.plugins.gui"))
            {
                SharedPtr<ISystemPlugin> guiPlugin = 
                    dynamic_pointer_cast<ISystemPlugin>(_plugins.loadPlugin(_settings[L"ximera.plugins.gui"]));

                if (guiPlugin)
                {
                    _guiSystem = SharedPtr<gui::IGuiSystem>(
                        dynamic_cast<gui::IGuiSystem*>(guiPlugin->createSystem(*this)));

                    XIMERA_CHECK(_guiSystem, XIMERA_THROW(InvalidParamException, L"Gui plugin could not create Gui system"));
                }
            }

            if (_settings.contains(L"ximera.plugins.physics"))
            {
                SharedPtr<ISystemPlugin> physicsPlugin = 
                    dynamic_pointer_cast<ISystemPlugin>(_plugins.loadPlugin(_settings[L"ximera.plugins.physics"]));

                if (physicsPlugin)
                {
                    _physicsSystem = SharedPtr<physics::IPhysicsSystem>(
                        dynamic_cast<physics::IPhysicsSystem*>(physicsPlugin->createSystem(*this)));

                    XIMERA_CHECK(_physicsSystem, XIMERA_THROW(InvalidParamException, L"Physics plugin could not create Physics system"));
                }
            }
        }
        catch (ximera::Exception& e)
        {
            // Rethrow our own exception if a plugin threw one
            // This will guarantee the handler of the exception to be safe
            throw ximera::Exception(e.fileName(), e.function(), e.line(), e.type(), e.message());
        }
        catch (std::exception& e)
        {
            XIMERA_THROW(ximera::Exception, L"Exception caught: " + string_util::str2wstr(e.what()));
        }
        catch (...)
        {
            XIMERA_THROW(ximera::Exception, L"Unknown exception caught!");
        }
    }
}}
